/*
 * File:    ximamng.cpp
 * Purpose:    Platform Independent MNG Image Class Loader and Writer
 * Author:    07/Aug/2001 Davide Pizzolato - www.xdp.it
 * CxImage version 7.0.1 07/Jan/2011
 */

#include "ximamng.h"

#if CXIMAGE_SUPPORT_MNG

////////////////////////////////////////////////////////////////////////////////
// callbacks for the mng decoder:
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// memory allocation; data must be zeroed
static mng_ptr
mymngalloc( mng_size_t size )
{
    return (mng_ptr)calloc(1, size);
}

////////////////////////////////////////////////////////////////////////////////
// memory deallocation
static void mymngfree(mng_ptr p, mng_size_t size)
{
    free(p);
}

////////////////////////////////////////////////////////////////////////////////
// Stream open/close:
// since the user is responsible for opening and closing the file,
// we leave the default implementation open
static mng_bool mymngopenstream(mng_handle mng)      { return MNG_TRUE; }
static mng_bool mymngopenstreamwrite(mng_handle mng) { return MNG_TRUE; }
static mng_bool mymngclosestream(mng_handle mng)     { return MNG_TRUE; }

////////////////////////////////////////////////////////////////////////////////
// feed data to the decoder
static mng_bool mymngreadstream(mng_handle mng, mng_ptr buffer, mng_uint32 size, mng_uint32 *bytesread)
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    // read the requested amount of data from the file
    *bytesread = mymng->file->Read( buffer, sizeof(uint8_t), size);
    return MNG_TRUE;
}

////////////////////////////////////////////////////////////////////////////////
static mng_bool mymngwritestream (mng_handle mng, mng_ptr pBuf, mng_uint32 iSize, mng_uint32 *iWritten)
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    // write it
    *iWritten = mymng->file->Write (pBuf, 1, iSize);
    return MNG_TRUE;
}

////////////////////////////////////////////////////////////////////////////////
// the header's been read. set up the display stuff
static mng_bool mymngprocessheader( mng_handle mng, mng_uint32 width, mng_uint32 height )
{
    // normally the image buffer is allocated here,
    // but in this module we don't know nothing about
    // the final environment.

    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    
    mymng->width  = width;
    mymng->height = height;
    mymng->bpp    = 24;
    mymng->effwdt = ((((width * mymng->bpp) + 31) >> 5) << 2);

    if (mng->bUseBKGD){
        mymng->nBkgndIndex = 0;
        mymng->nBkgndColor.rgbRed  = mng->iBGred >> 8;
        mymng->nBkgndColor.rgbGreen =mng->iBGgreen >> 8;
        mymng->nBkgndColor.rgbBlue = mng->iBGblue >> 8;
    }

    mymng->image = (uint8_t*)malloc(height * mymng->effwdt);

    // tell the mng decoder about our bit-depth choice
#if CXIMAGE_SUPPORT_ALPHA
    mng_set_canvasstyle( mng, MNG_CANVAS_RGB8_A8 );
    mymng->alpha = (uint8_t*)malloc(height * width);
#else
    mng_set_canvasstyle( mng, MNG_CANVAS_BGR8);
    mymng->alpha = nullptr;
#endif
    return MNG_TRUE;
}

////////////////////////////////////////////////////////////////////////////////
// return a row pointer for the decoder to fill
static mng_ptr mymnggetcanvasline( mng_handle mng, mng_uint32 line )
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    return (mng_ptr)(mymng->image + (mymng->effwdt * (mymng->height - 1 - line)));
}
////////////////////////////////////////////////////////////////////////////////
// return a row pointer for the decoder to fill for alpha channel
static mng_ptr mymnggetalphaline( mng_handle mng, mng_uint32 line )
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    return (mng_ptr)(mymng->alpha + (mymng->width * (mymng->height - 1 - line)));
}

////////////////////////////////////////////////////////////////////////////////
// timer
static mng_uint32 mymnggetticks(mng_handle mng)
{
#ifdef WIN32
    return (mng_uint32)GetTickCount();
#else
  return 0;
#endif
}

////////////////////////////////////////////////////////////////////////////////
// Refresh: actual frame need to be updated (Invalidate)
static mng_bool mymngrefresh(mng_handle mng, mng_uint32 x, mng_uint32 y, mng_uint32 w, mng_uint32 h)
{
//    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    return MNG_TRUE;
}

////////////////////////////////////////////////////////////////////////////////
// interframe delay callback
static mng_bool mymngsettimer(mng_handle mng, mng_uint32 msecs)
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(mng);
    mymng->delay = msecs;     // set the timer for when the decoder wants to be woken
    return MNG_TRUE;
}

////////////////////////////////////////////////////////////////////////////////
static mng_bool mymngerror(mng_handle mng, mng_int32 code, mng_int8 severity, mng_chunkid chunktype, mng_uint32 chunkseq, mng_int32 extra1, mng_int32 extra2, mng_pchar text)
{
    return mng_cleanup(&mng); //<Arkadiy Olovyannikov>
}

////////////////////////////////////////////////////////////////////////////////
// CxImage members
////////////////////////////////////////////////////////////////////////////////
CxImageMNG::CxImageMNG(): CxImage(CXIMAGE_FORMAT_MNG)
{
    hmng = nullptr;
    memset(&mnginfo,0,sizeof(mngstuff));
    mnginfo.nBkgndIndex = -1;
    mnginfo.speed = 1.0f;
}
////////////////////////////////////////////////////////////////////////////////
CxImageMNG::~CxImageMNG()
{
    // cleanup and return
    if (mnginfo.thread){ //close the animation thread
        mnginfo.animation_enabled=0;
        ResumeThread(mnginfo.thread);
        WaitForSingleObject(mnginfo.thread,500);
        CloseHandle(mnginfo.thread);
    }
    // free objects
    if (mnginfo.image) free(mnginfo.image);
    if (mnginfo.alpha) free(mnginfo.alpha);
    if (hmng) mng_cleanup(&hmng); //be sure it's not needed any more. (active timers ?)
}
////////////////////////////////////////////////////////////////////////////////
void CxImageMNG::SetCallbacks(mng_handle mng)
{
    // set the callbacks
    mng_setcb_errorproc(mng, mymngerror);
    mng_setcb_openstream(mng, mymngopenstream);
    mng_setcb_closestream(mng, mymngclosestream);
    mng_setcb_readdata(mng, mymngreadstream);
    mng_setcb_processheader(mng, mymngprocessheader);
    mng_setcb_getcanvasline(mng, mymnggetcanvasline);
    mng_setcb_refresh(mng, mymngrefresh);
    mng_setcb_gettickcount(mng, mymnggetticks);
    mng_setcb_settimer(mng, mymngsettimer);
    mng_setcb_refresh(mng, mymngrefresh);
    mng_setcb_getalphaline(mng, mymnggetalphaline);
}
////////////////////////////////////////////////////////////////////////////////
#if CXIMAGE_SUPPORT_DECODE
////////////////////////////////////////////////////////////////////////////////
// can't use the CxImage implementation because it looses mnginfo
bool CxImageMNG::Load(const TCHAR * imageFileName){
    FILE* hFile;    //file handle to read the image
#ifdef WIN32
    if ((hFile=_tfopen(imageFileName,_T("rb")))==nullptr)  return false;    // For UNICODE support
#else
    if ((hFile=fopen(imageFileName,"rb"))==nullptr)  return false;
#endif
    bool bOK = Decode(hFile);
    fclose(hFile);
    return bOK;
}
////////////////////////////////////////////////////////////////////////////////
bool CxImageMNG::Decode(CxFile *hFile)
{
    if (hFile == nullptr) return false;

    cx_try
    {
        // set up the mng decoder for our stream
        hmng = mng_initialize(&mnginfo, (mng_memalloc)mymngalloc, (mng_memfree)mymngfree, MNG_NULL);
        if (hmng == nullptr) cx_throw("could not initialize libmng");            

        // set the file we want to play
        mnginfo.file = hFile;

        // Set the colorprofile, lcms uses this:
        mng_set_srgb(hmng, MNG_TRUE );
        // Set white as background color:
        uint16_t Red,Green,Blue;
        Red = Green = Blue = (255 << 8) + 255;
        mng_set_bgcolor(hmng, Red, Green, Blue );
        // If PNG Background is available, use it:
        mng_set_usebkgd(hmng, MNG_TRUE );

        // No need to store chunks:
        mng_set_storechunks(hmng, MNG_FALSE);
        // No need to wait: straight reading
        mng_set_suspensionmode(hmng, MNG_FALSE);

        SetCallbacks(hmng);

        mng_datap pData = (mng_datap)hmng;

        // read in the image
        info.nNumFrames=0;
        int32_t retval=MNG_NOERROR;

        retval = mng_readdisplay(hmng);

        if (retval != MNG_NOERROR && retval != MNG_NEEDTIMERWAIT){
            mng_store_error(hmng,retval,0,0);
            if (hmng->zErrortext){
                cx_throw(hmng->zErrortext);
            } else {
                cx_throw("Error in MNG file");
            }
        }

        if (info.nEscape == -1) {
            // Return output dimensions only
            head.biWidth = hmng->iWidth;
            head.biHeight = hmng->iHeight;
            info.dwType = CXIMAGE_FORMAT_MNG;
            return true;
        }

        // read all
        while(pData->bReading){
            retval = mng_display_resume(hmng);
            info.nNumFrames++;
        }

        // single frame check:
        if (retval != MNG_NEEDTIMERWAIT){
            info.nNumFrames--;
        } else {
            mnginfo.animation=1;
        }

        if (info.nNumFrames<=0) info.nNumFrames=1;

        if (mnginfo.animation_enabled==0){
            // select the frame
            if (info.nFrame>=0 && info.nFrame<info.nNumFrames){
                for (int32_t n=0;n<info.nFrame;n++) mng_display_resume(hmng);
            } else cx_throw("Error: frame not present in MNG file");
        }

        if (mnginfo.nBkgndIndex >= 0){
            info.nBkgndIndex = mnginfo.nBkgndIndex;
            info.nBkgndColor.rgbRed = mnginfo.nBkgndColor.rgbRed;
            info.nBkgndColor.rgbGreen = mnginfo.nBkgndColor.rgbGreen;
            info.nBkgndColor.rgbBlue = mnginfo.nBkgndColor.rgbBlue;
        }

        //store the newly created image
        if (Create(mnginfo.width,mnginfo.height,mnginfo.bpp, CXIMAGE_FORMAT_MNG)){
            memcpy(GetBits(), mnginfo.image, info.dwEffWidth * head.biHeight);
#if CXIMAGE_SUPPORT_ALPHA
            SwapRGB2BGR();
            AlphaCreate();
            if(AlphaIsValid() && mnginfo.alpha){
                memcpy(AlphaGetPointer(),mnginfo.alpha,mnginfo.width * mnginfo.height);
            }
#endif
        } else cx_throw("CxImageMNG::Decode cannot create image");


    } cx_catch {
        if (strcmp(message,"")) strncpy(info.szLastError,message,255);
        return false;
    }
    return true;
}
////////////////////////////////////////////////////////////////////////////////
#endif //CXIMAGE_SUPPORT_DECODE
////////////////////////////////////////////////////////////////////////////////
#if CXIMAGE_SUPPORT_ENCODE
////////////////////////////////////////////////////////////////////////////////
bool CxImageMNG::Encode(CxFile *hFile)
{
    if (EncodeSafeCheck(hFile)) return false;

    cx_try
    {
        if (head.biClrUsed != 0) cx_throw("MNG encoder can save only RGB images");
        // set the file we want to play
        mnginfo.file = hFile;
        mnginfo.bpp = head.biBitCount;
        mnginfo.effwdt = info.dwEffWidth;
        mnginfo.height = head.biHeight;
        mnginfo.width =  head.biWidth;

        mnginfo.image = (uint8_t*)malloc(head.biSizeImage);
        if (mnginfo.image == nullptr) cx_throw("could not allocate memory for MNG");
        memcpy(mnginfo.image,info.pImage, head.biSizeImage);

        // set up the mng decoder for our stream
        hmng = mng_initialize(&mnginfo, (mng_memalloc)mymngalloc, (mng_memfree)mymngfree, MNG_NULL);
        if (hmng == nullptr) cx_throw("could not initialize libmng");            

        mng_setcb_openstream(hmng, mymngopenstreamwrite );
        mng_setcb_closestream(hmng, mymngclosestream);
        mng_setcb_writedata(hmng, mymngwritestream);

        // Write File:
           mng_create(hmng);
        // Just a single Frame (save a normal PNG):
        WritePNG(hmng, 0, 1 );
        // Now write file:
        mng_write(hmng);

    } cx_catch {
        if (strcmp(message,"")) strncpy(info.szLastError,message,255);
        return false;
    }
    return true;
}
////////////////////////////////////////////////////////////////////////////////
// Writes a single PNG datastream
void CxImageMNG::WritePNG( mng_handle hMNG, int32_t Frame, int32_t FrameCount )
{
    mngstuff *mymng = (mngstuff *)mng_get_userdata(hMNG);
    
    int32_t OffsetX=0,OffsetY=0,OffsetW=mymng->width,OffsetH=mymng->height;

    uint8_t *tmpbuffer = new uint8_t[ (mymng->effwdt+1) * mymng->height];
    if( tmpbuffer == 0 ) return;

    // Write DEFI chunk.
    mng_putchunk_defi( hMNG, 0, 0, 0, MNG_TRUE, OffsetX, OffsetY, MNG_FALSE, 0, 0, 0, 0 );
          
    // Write Header:
    mng_putchunk_ihdr(
        hMNG, 
        OffsetW, OffsetH, 
        MNG_BITDEPTH_8, 
        MNG_COLORTYPE_RGB, 
        MNG_COMPRESSION_DEFLATE, 
        MNG_FILTER_ADAPTIVE, 
        MNG_INTERLACE_NONE 
    );

    // transfer data, add Filterbyte:
    for( int32_t Row=0; Row<OffsetH; Row++ ){
        // First Byte in each Scanline is Filterbyte: Currently 0 -> No Filter.
        tmpbuffer[Row*(mymng->effwdt+1)]=0; 
        // Copy the scanline: (reverse order)
        memcpy(tmpbuffer+Row*(mymng->effwdt+1)+1, 
            mymng->image+((OffsetH-1-(OffsetY+Row))*(mymng->effwdt))+OffsetX,mymng->effwdt);
        // swap red and blue components
        RGBtoBGR(tmpbuffer+Row*(mymng->effwdt+1)+1,mymng->effwdt);
    } 

    // Compress data with ZLib (Deflate):
    uint8_t *dstbuffer = new uint8_t[(mymng->effwdt+1)*OffsetH];
    if( dstbuffer == 0 ) return;
    uint32_t dstbufferSize=(mymng->effwdt+1)*OffsetH;

    // Compress data:
    if(Z_OK != compress2((Bytef *)dstbuffer,(ULONG *)&dstbufferSize,(const Bytef*)tmpbuffer,
                        (ULONG) (mymng->effwdt+1)*OffsetH,9 )) return;

    // Write Data into MNG File:
    mng_putchunk_idat( hMNG, dstbufferSize, (mng_ptr*)dstbuffer);
    mng_putchunk_iend(hMNG);

    // Free the stuff:
    delete [] tmpbuffer;
    delete [] dstbuffer;
}
////////////////////////////////////////////////////////////////////////////////
int32_t CxImageMNG::Resume()
{
    if (MNG_NEEDTIMERWAIT == mng_display_resume(hmng)){
        if (info.pImage==nullptr){
            Create(mnginfo.width,mnginfo.height,mnginfo.bpp, CXIMAGE_FORMAT_MNG);
        }
        if (IsValid()){
            memcpy(GetBits(), mnginfo.image, info.dwEffWidth * head.biHeight);
#if CXIMAGE_SUPPORT_ALPHA
            SwapRGB2BGR();
            AlphaCreate();
            if(AlphaIsValid() && mnginfo.alpha){
                memcpy(AlphaGetPointer(),mnginfo.alpha,mnginfo.width * mnginfo.height);
            }
#endif
        }
    } else {
        mnginfo.animation_enabled = 0;
    }
    return mnginfo.animation_enabled;
}
////////////////////////////////////////////////////////////////////////////////
void CxImageMNG::SetSpeed(float speed)
{
    if (speed>10.0) mnginfo.speed = 10.0f;
    else if (speed<0.1) mnginfo.speed = 0.1f;
    else mnginfo.speed=speed;
}
////////////////////////////////////////////////////////////////////////////////
#endif //CXIMAGE_SUPPORT_ENCODE
////////////////////////////////////////////////////////////////////////////////
#endif // CXIMAGE_SUPPORT_MNG
